package cn.ycc1.functionlibrary.reflection;

/**
 * Creating an Interceptor with Annotations
 * @author ycc
 * @date 2025/3/9
 * The first example you are going to run consists in designing a simple interception system for service methods. This is a
 * system that exists in Java EE, Jakarta EE, and other enterprise application frameworks. The concept is simple: you add an
 * annotation on a method, this annotation takes a class as an attribute, and a specific method of this class is called instead
 * of your service method.
 */
public class Interceptor {
    /**
     * What is an Interceptor?
     * The concept of intercepting a method call is very simple. it relies on two elements: a service that has a service method,
     * and an interceptor, that also carries a method. When you call your service method, what you want is that this interceptor
     * method is called instead of it.
     *
     * Then, this method can do many things.
     *
     * It can validate the arguments sent to your service method.
     * It can change these arguments.
     * Beside some validation, it can also enforce some security rules.
     * It can decide to call your service method, or not.
     * It can then get the result of your service method, and decide to return it as is, or to modify it.
     * And it can catch the exceptions thrown by your service method, and act upon them.
     * This kind of interceptor can be implemented using Proxy objects, that are beyond the scope of this chapter. So we are
     * going to implement this feature in a simpler way.
     */

    /**
     * Designing the Intercept Interface and Annotation
     * Let us start with the annotation you need. This annotation needs to tell you what class of interceptor is to be used.
     * For the sake of simplicity, we are going to impose that interceptors implement a specific interface.
     *
     * Note that this interface uses parameterized types for the object you intercept, and the returned type of the method you
     * intercept.
     *
     * public interface Interceptor<T, R> {
     *     R intercept(T interceptedObject, Method method, Object... arguments);
     * }
     * Then you can design the annotation you need.
     *
     * @Target(ElementType.METHOD)
     * @Retention(RetentionPolicy.RUNTIME)
     * public @interface Intercept {
     *     Class<? extends Interceptor<?, ?>> value();
     * }
     */

    /**
     * Designing a Service and an Intercepted Method
     * Let us now create a service, with a method that you need to intercept. The MessageInterceptor class is an implementation
     * of the Interceptor interface that you are going to write in the next section.
     *
     * public class SomeInterceptedService {
     *
     *     @Intercept(MessageInterceptor.class)
     *     public String message(String input) {
     *         return input.toUpperCase();
     *     }
     * }
     * Having written this code, what you expect is the following.
     *
     * You need a way to call the message() method of your SomeInterceptedService class.
     * When you call it, you expect your implementation of the intercept() method from your MessageInterceptor class to be
     * called before the message() method is called.
     * You then expect to get the result of this call.
     * The code you write can look like the following. Note that the invoke() method takes the class of your service, the name
     * of the method, and the arguments you need to pass to this method. In a Proxy based system, this call would be much
     * simpler, as a dynamic proxy can build implementations for you. So your call would look like a regular method call on a
     * regular class.
     *
     * void main() {
     *     String output = ServiceFactory.invoke(SomeInterceptedService.class, "message", "Hello");
     *     System.out.println("output = " + output);
     * }
     * At this point you need to write two classes: the MessageInterceptor class, and the ServiceFactory class.
     */

    /**
     * Writing the MessageInterceptor Class
     * This class is an implementation of the Interceptor interface that has the responsibility to decide, or not, to call your
     * intercepted service.
     *
     * You could write it specifically to call the method of the service you need. What we do here, is that the intercepted
     * method is a parameter of the intercept() method. So, as you saw in the previous sections of this chapter, you need three
     * elements to reflectively call this method:
     *
     * The method itself, in the form of a Method object. Note that you could also pass the name of this method.
     * The object on which this method is invoked. This is actually your service.
     * And the arguments you need to pass to this method.
     * Then what you need to do in this method is to implement your business logic: why do you need to intercept this method,
     * what do you need to do in this interceptor?
     *
     * Here, we are first doing some argument validation. The arguments array should contain only one element of type String.
     * If this is not the case, we throw an exception, and the intercepted method is not called.
     *
     * Then we call the intercepted method and get the result it produced. Note that we could have modified the arguments passed
     * to this method, if needed.
     *
     * Then we modify the result by adding the [was intercepted] message to it.
     *
     * public class MessageInterceptor implements Interceptor<SomeInterceptedService, String> {
     *
     *     @Override
     *     public String intercept(
     *             SomeInterceptedService service, Method interceptedMethod, Object... arguments) {
     *         try {
     *             if (arguments.length == 1) {
     *
     *                 // validating the arguments
     *                 String input = (String) arguments[0];
     *                 Objects.requireNonNull(input, "Input is null");
     *                 if (input.isEmpty()) {
     *                     throw new IllegalArgumentException("Input is empty");
     *                 }
     *
     *                 // calling the service method
     *                 String result = (String)interceptedMethod.invoke(service, arguments);
     *                 return result + " [was intercepted]";
     *             }
     *         } catch (IllegalAccessException | InvocationTargetException e) {
     *             throw new RuntimeException(e);
     *         }
     *         throw new IllegalArgumentException(
     *                 "Arguments should contain exactly one argument of type String");
     *     }
     * }
     */

    /**
     * Writing the ServiceFactory Class
     * This class is more technical, and leverages the Reflection API.
     *
     * It takes the class on which the method is declared, the name of the method that is to be intercepted, and the arguments
     * you need to pass to this method.
     *
     * First, it needs to create an instance of the service. This is done by getting the empty constructor of this service.
     * This constructor needs to be there, if not an exception is thrown.
     *
     * Then, it needs to locate the method it needs to call. For that, it needs two elements: the name of the method, and the
     * types of its parameters in an array. This is what the stream is doing: it creates an array of classes from the array of
     * arguments. With these two elements, it can then locate the right method.
     *
     * Then, it needs to check if the annotation is present on this method. If it is, then it needs to get the instance of this
     * annotation and to call the value() method on it, that returns the class of the interceptor. Note that you need to cast
     * it to the right type, as annotations do not support generics. This cast will issue a compiler warning, as the compiler
     * cannot check if it is correct or not.
     *
     * Once it knows the class of the interceptor, it needs to create an instance of it, to call its intercept() method with
     * the right arguments. This is a direct call, not a reflective call.
     *
     * Note that if the annotation is not found, then it reflectively calls the service method, without any interception.
     *
     * In both cases, it needs to get the returned object, and to return it.
     *
     * public class ServiceFactory {
     *     public static <T, R> R invoke(
     *             Class<? extends T> serviceClass, String methodName, Object... arguments) {
     *         try {
     *             // Getting an instance of the service
     *             T service = serviceClass.getConstructor().newInstance();
     *
     *             // Locating the service method
     *             Class<?>[] parameterClasses =
     *                     Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
     *             Method method = serviceClass.getDeclaredMethod(methodName, parameterClasses);
     *
     *             // locating the Intercept annotation
     *             if (method.isAnnotationPresent(Intercept.class)) {
     *                 Intercept intercept = method.getDeclaredAnnotation(Intercept.class);
     *                 Class<? extends Interceptor<T, R>> interceptorClass =
     *                         (Class<? extends Interceptor<T, R>>) intercept.value();
     *
     *                 // creating an instance of the interceptor
     *                 Interceptor<T, R> interceptor = interceptorClass.getConstructor().newInstance();
     *
     *                 // intercepting the service method
     *                 R returnedObject = interceptor.intercept(service, method, arguments);
     *                 return returnedObject;
     *             } else {
     *                 // invoking the service method
     *                 R returnedObject = (R) method.invoke(service, arguments);
     *                 return returnedObject;
     *             }
     *         } catch (InstantiationException | IllegalAccessException |
     *                  InvocationTargetException | NoSuchMethodException e) {
     *             throw new RuntimeException(e);
     *         }
     *     }
     * }
     * As you can see, this code is rather complex, somehow obscure, and not type safe at compile time. This is most of the time
     * the case when you are using the Reflection API on real application use cases.
     *
     * Now that you have all the elements, you can finally run this code.
     *
     * Let us add another class, to show that the interception mechanism is working correctly. This SomeNonInterceptedService
     * class is the same as the SomeInterceptedService class. The only difference is that its service method does not declare
     * the interceptor.
     *
     * public static class SomeNonInterceptedService {
     *
     *     String message(String input) {
     *         return input.toUpperCase();
     *     }
     * }
     * Now that you have all the elements, you can run the following code.
     *
     * public static void main(String[] args) {
     *
     *     String interceptedOutput =
     *             ServiceFactory.invoke(SomeInterceptedService.class, "message", "Hello");
     *     System.out.println("Intercepted output = " + interceptedOutput);
     *
     *     String nonInterceptedOutput =
     *             ServiceFactory.invoke(SomeNonInterceptedService.class, "message", "Hello");
     *     System.out.println("Non intercepted output = " + nonInterceptedOutput);
     * }
     * It prints the following result.
     *
     * Intercepted output = HELLO [was intercepted]
     * Non intercepted output = HELLO
     */


}
